﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Data.Schema;
using Microsoft.Data.Schema.Build;
using Microsoft.Data.Schema.Extensibility;
using Microsoft.Data.Schema.SchemaModel;
using System.IO;
using System.Security.Permissions;
using System.Diagnostics;
using System.IO.Packaging;
using System.Globalization;

namespace DeploymentCustomActions
{
    [Serializable]
    public class VsdbCmd10
    {
        public VsdbCmd10()
        {
            SqlCmdVariables = new List<SqlCmdVariable>();
        }

        public string Id { get; set; }
        public string ComponentName { get; set; }
        public string DataSchemaProvider { get; set; }
        public string DeploymentConfigurationFile { get; set; }
        public string ModelFile { get; set; }
        public string ConnectionString { get; set; }
        public List<SqlCmdVariable> SqlCmdVariables { get; set; }
        public string DatabaseName { get; set; }
        public string SqlCmdVarsFile { get; set; }
        public string PostDeploymentScript { get; set; }
        public string PreDeploymentScript { get; set; }

        public string GenerateManifestXml()
        {
            return String.Format(CultureInfo.InvariantCulture, @"<?xml version=""1.0"" encoding=""utf-8""?>
                            <Project ToolsVersion=""4.0"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
                              <PropertyGroup>
                                <TargetConnectionString>{0}</TargetConnectionString>
                                <TargetDatabase>{1}</TargetDatabase>
                                <DeployToDatabase>True</DeployToDatabase>
                                <DeployToScript>True</DeployToScript>
                                <SourceModel>{2}</SourceModel>
                                <DeployScriptFileName>{1}.sql</DeployScriptFileName>
                                <DeploymentConfigurationFile>{3}</DeploymentConfigurationFile>
                              </PropertyGroup>

                              <PropertyGroup>
                                <SqlCommandVariablesFile>{4}</SqlCommandVariablesFile>
                              </PropertyGroup>

                              <ItemGroup>
                                <DeploymentExtensionConfiguration Include=""{5}"">
                                  <__PostdeploymentMetadata>
                                  </__PostdeploymentMetadata>
                                </DeploymentExtensionConfiguration>
                                <DeploymentExtensionConfiguration Include=""{6}"">
                                  <__PredeploymentMetadata>
                                  </__PredeploymentMetadata>
                                </DeploymentExtensionConfiguration>
                              </ItemGroup>

                              {7}
                           </Project>", ConnectionString, DatabaseName, 
                                    ModelFile, DeploymentConfigurationFile, SqlCmdVarsFile, 
                                    PostDeploymentScript, PreDeploymentScript,
                                    GetExtensionProperties());
        }

        private string GetExtensionProperties()
        {
            switch (this.DataSchemaProvider)
            {
                case "SQL":
                    return @"       <ItemGroup>
                                <DeploymentExtension Include=""Microsoft.Data.Schema.Sql.Build.SqlPlanOrderModifier"">
                                  <Assembly>Microsoft.Data.Schema.Sql</Assembly>
                                  <Version>10.0.0.0</Version>
                                  <Token>sD9ffxHVCjo=</Token>
                                </DeploymentExtension>
                                <DeploymentExtension Include=""Microsoft.Data.Schema.Sql.Build.SqlPrePostDeploymentModifier"">
                                  <Assembly>Microsoft.Data.Schema.Sql</Assembly>
                                  <Version>10.0.0.0</Version>
                                  <Token>sD9ffxHVCjo=</Token>
                                </DeploymentExtension>
                                <DeploymentExtension Include=""Microsoft.Data.Schema.Sql.Refactoring.SqlRefactoringDeploymentContributor"">
                                  <Assembly>Microsoft.Data.Schema.Sql</Assembly>
                                  <Version>10.0.0.0</Version>
                                  <Token>sD9ffxHVCjo=</Token>
                                </DeploymentExtension>
                              </ItemGroup>";
                default:
                    return "";
            }
        }
    }

    [Serializable]    
    public class SqlCmdVariable
    {
        public string Name { get; set; }
        public string Value { get; set; }
    }

    /// <summary>
    /// Custom Actions to Manage Databases.
    /// </summary>
    public static class InstallDatabase
    {
        [CustomAction]
        public static ActionResult CreateDatabaseExecute(Session session)
        {
            if (session == null) return ActionResult.Failure;

            List<VsdbCmd10> databases = ParseVsDbCmd10Table(session);

            var databasesToInstall = databases.Join(session.Components, s => s.ComponentName, s => s.Name, (x, y) => new { x, y })
                                              .Where(s => s.y.CurrentState == InstallState.Absent && s.y.RequestState == InstallState.Local)
                                              .Select(s => s.x);

            if (databasesToInstall.Any())
            {
                string vsDbCmdPath = System.IO.Path.GetTempFileName();
                string vsDbCmdFolder = Constants.VsDbCmd10.GetExtractFolder(vsDbCmdPath);

                CustomActionData customActionData = new CustomActionData();
                customActionData.Add(Constants.VsDbCmd10.VsDbCmdBinaryPath, vsDbCmdFolder);
                customActionData.AddObject(Constants.VsDbCmd10.VsDbCmdBinaryContent, databasesToInstall.ToList());
                
                Utility.ExtractContentFromBinaryTable(session, "VSDBCMD10", vsDbCmdPath);
                Utility.Unzip(vsDbCmdPath, vsDbCmdFolder);

                session.DoAction("DeployDatabaseDeferred", customActionData);
            }
            return ActionResult.Success;
        }

        [CustomAction]
        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        public static ActionResult DeployDatabaseDeferred(Session session)
        {
            if (session == null) return ActionResult.Failure;

            string vsDbCmdPath = session.CustomActionData[Constants.VsDbCmd10.VsDbCmdBinaryPath];
            List<VsdbCmd10> databases = session.CustomActionData.GetObject<List<VsdbCmd10>>(Constants.VsDbCmd10.VsDbCmdBinaryContent);

            if (databases.Any(s => DeployVsDbCmdDatabase(s, vsDbCmdPath, session) != 0))
                return ActionResult.Failure;

            return ActionResult.Success;
        }

        private static List<VsdbCmd10> ParseVsDbCmd10Table(Session session)
        {
            Dictionary<string, VsdbCmd10> result = new Dictionary<string, VsdbCmd10>();
            using (View view = session.Database.OpenView(Constants.VsDbCmd10.Query))
            {
                view.Execute();
                foreach (Record record in view) using (record)
                    {
                        string id = record.GetString(Constants.VsDbCmd10.Id);
                        string componenetName = session.Format(record.GetString(Constants.VsDbCmd10.ComponentName));
                        string dataSchemaProvider = session.Format(record.GetString(Constants.VsDbCmd10.DatabaseSchemaProvider));
                        string deploymentConfigurationFile = session.Format(record.GetString(Constants.VsDbCmd10.DeploymentConfigurationFile));
                        string modelFile = session.Format(record.GetString(Constants.VsDbCmd10.ModelFile));
                        string connectionString = session.Format(record.GetString(Constants.VsDbCmd10.ConnectionString));
                        string databaseName = session.Format(record.GetString(Constants.VsDbCmd10.DatabaseName));
                        string sqlCmdVarFiles = session.Format(record.GetString(Constants.VsDbCmd10.SqlCmdVarsFile));
                        string postDeploymentScript = session.Format(record.GetString(Constants.VsDbCmd10.PostDeploymentScript));
                        string preDeploymentScript = session.Format(record.GetString(Constants.VsDbCmd10.PreDeploymentScript));
                        

                        VsdbCmd10 vsDbCmd = new VsdbCmd10()
                        {
                            Id = id,
                            ComponentName = componenetName,
                            DataSchemaProvider = dataSchemaProvider,
                            DeploymentConfigurationFile = deploymentConfigurationFile,
                            ModelFile = modelFile,
                            ConnectionString = connectionString,
                            DatabaseName = databaseName,
                            SqlCmdVarsFile = sqlCmdVarFiles,
                            PostDeploymentScript = postDeploymentScript,
                            PreDeploymentScript = preDeploymentScript
                        };                        
                        result.Add(id, vsDbCmd);
                    }
            }
            ParseVsDbCmd10SqlCmdVariables(session, ref result);
            return result.Values.ToList();
        }

        private static void ParseVsDbCmd10SqlCmdVariables(Session session, ref Dictionary<string, VsdbCmd10> factory)
        {
            using (View view = session.Database.OpenView(Constants.VsDbCmd10.SqlCmdVariablesQuery))
            {
                view.Execute();
                foreach (Record record in view) using (record)
                    {
                        string vsDbCmdId = record.GetString(Constants.VsDbCmd10.VsDbCmd10Id);
                        string name = session.Format(record.GetString(Constants.VsDbCmd10.Name));
                        string value = session.Format(record.GetString(Constants.VsDbCmd10.Value));
                        
                        SqlCmdVariable sqlCmdVariable = new SqlCmdVariable()
                        {
                            Name = name,
                            Value = value
                        };
                        factory[vsDbCmdId].SqlCmdVariables.Add(sqlCmdVariable);
                    }
            }
        }

        private static int ExecuteVsDbCmdTask(string arguments, string vsDbCmdPath, Session session)
        {
            session.Log("\n\nExecuting VsDbCmd.exe {0}\n\n", arguments);
            Process process = null;
            try
            {
                process = new Process();
                process.StartInfo.WorkingDirectory = vsDbCmdPath;
                process.StartInfo.Arguments = arguments;
                process.StartInfo.FileName = vsDbCmdPath + "\\VsDbCmd.exe";
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;

                process.OutputDataReceived += (x, y) => session.Log(y.Data ?? String.Empty);
                process.ErrorDataReceived += (x, y) => session.Log(y.Data ?? String.Empty);
                process.EnableRaisingEvents = true;
                process.Start();
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();
                process.WaitForExit();
                int exitCode = process.ExitCode;
                return exitCode;
            }
            finally
            {
                process.Close();
            }
        }

        private static int DeployVsDbCmdDatabase(VsdbCmd10 database, string vsDbCmdPath, Session session)
        {
            string manifestFile = System.IO.Path.GetTempFileName();

            using (System.IO.StreamWriter fs = new StreamWriter(manifestFile, false))
            {
                string manifestXml = database.GenerateManifestXml();
                fs.Write(manifestXml);
            }

            string arguments = String.Format(CultureInfo.InvariantCulture, @"/a:Deploy /manifest:""{0}""", manifestFile);
            database.SqlCmdVariables.ForEach(s => arguments = String.Format(CultureInfo.InvariantCulture, "{0} /p:{1}=\"{2}\"", arguments, s.Name, s.Value));
            return ExecuteVsDbCmdTask(arguments, vsDbCmdPath, session);
        }
    }
}